package cn.claim.storage.server;

import cn.claim.storage.tools.*;

import java.io.IOException;
import java.io.PrintWriter;
import java.net.*;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * Created by claim on 17-7-13.
 */

public final class FileServer {

    private static final List<NodeData> nodes = new CopyOnWriteArrayList<>();

    private static class CountDown extends TimerTask {
        public CountDown() {
        }

        @Override
        public void run() {
            nodes.sort((NodeData a, NodeData b) -> {
                long x = a.getSpareSpace(), y = b.getSpareSpace();
                if (x > y)
                    //reverse order
                    return -1;
                else if (x < y)
                    return 1;
                return 0;
            });
            nodes.forEach(nodeData -> {
                nodeData.downSurvival();
                if (nodeData.isDead()) {
                    nodes.remove(nodeData);
                    System.out.println("node removed");
                }
            });
        }
    }

    private static class UDPListener extends Thread {
        private final DatagramSocket UDPSocket;
        private final DatagramPacket UDPPacket = new DatagramPacket(new byte[1024], 1024);

        public UDPListener(DatagramSocket UDPSocket) {
            this.UDPSocket = UDPSocket;
        }

        @Override
        public void run() {
            while (true) {
                try {
                    UDPSocket.receive(UDPPacket);
                    String content = new String(UDPPacket.getData(), 0, UDPPacket.getLength());
                    System.out.println(content);
                    addNode(JSONParsable.parse(content, NodeData.class));
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private static class DataBaseListener extends Thread {
        private static final DataManager database = DataManager.DATABASE;
        private final Socket socket;

        public DataBaseListener(Socket socket) {
            this.socket = socket;
        }

        private void uploadHandler(String data, PrintWriter response) {
            FileInfoData fileInfoData = JSONParsable.parse(data, FileInfoData.class);
            database.update(fileInfoData.getUuid(), fileInfoData);
            if (Objects.equals(fileInfoData.getBackupServerName(), "")) {
                NodeData nodeData = null;
                for (NodeData nodeData1 : nodes) {
                    if (Objects.equals(fileInfoData.getServerName(), nodeData1.getName())) {
                        nodeData = nodeData1;
                        break;
                    }
                }
                if (nodeData != null) {
                    Socket socket = null;
                    int retry = 5;
                    try {
                        while (true) {
                            socket = new Socket(nodeData.getIp(), nodeData.getQueryPort());
                            PrintWriter _request = new PrintWriter(socket.getOutputStream());
                            Scanner _response = new Scanner(socket.getInputStream());

                            _request.println("backup");
                            _request.println(fileInfoData.getUuid());
                            NodeData backupNodeInfo = getIdleNode(nodeData);
                            if (backupNodeInfo != null) {
                                _request.println(backupNodeInfo.toJSON());
                            } else {
                                _request.println("null");
                            }
                            _request.flush();
                            String status = _response.nextLine();
                            if (Objects.equals(status, "finished")) {
                                database.update(fileInfoData.getUuid(),
                                        new FileInfoDataBuilder(fileInfoData)
                                                .withBackupServerName(backupNodeInfo.getName()).build());
                                response.println("true");
                                response.flush();
                                return;
                            }
                            if (--retry <= 0) {
                                response.println("true");
                                response.flush();
                                return;
                            }
                            socket.close();
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                    } finally {
                        try {
                            if (socket != null) {
                                socket.close();
                            }
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }

        private void deleteHandler(String data, PrintWriter response) {
            FileInfoData fileInfoData = database.select(data, FileInfoData.class);
            if (fileInfoData == null) {
                response.println("false");
                response.flush();
                return;
            }
            List<String> ips = new ArrayList<>(2);
            List<Integer> ports = new ArrayList<>(2);
            nodes.forEach(nodeData -> {
                if (Objects.equals(nodeData.getName(), fileInfoData.getServerName())
                        || Objects.equals(nodeData.getName(), fileInfoData.getBackupServerName())) {
                    ips.add(nodeData.getIp());
                    ports.add(nodeData.getQueryPort());
                }
            });
            try {
                Socket socket;
                for (int i = 0; i < ips.size() && i < 2; i++) {
                    socket = new Socket(ips.get(i), ports.get(i));
                    PrintWriter _request = new PrintWriter(socket.getOutputStream());

                    _request.println("delete");
                    _request.println(fileInfoData.getUuid());
                    _request.flush();
                    socket.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            response.println(database.delete(data));
            response.flush();
        }

        @Override
        public void run() {
            try {
                Scanner request = new Scanner(socket.getInputStream());
                PrintWriter response = new PrintWriter(socket.getOutputStream());
                String order = request.nextLine();
                String data = request.nextLine();
                switch (order) {
                    case "file": {
                        FileInfoData fileInfoData = database.select(data, FileInfoData.class);
                        response.println(fileInfoData == null ? "null" : fileInfoData.toJSON());
                        response.flush();
                        break;
                    }
                    case "node": {
                        if (Objects.equals(data, "null"))
                            response.println(getIdleNode(null));
                        else
                            response.println(getIdleNode(JSONParsable.parse(data, NodeData.class)));
                        response.flush();
                        break;
                    }
                    case "nodeInfo": {
                        boolean found = false;
                        for (NodeData nodeData : nodes) {
                            if (Objects.equals(nodeData.getName(), data)) {
                                response.println(nodeData.toJSON());
                                response.flush();
                                found = true;
                                break;
                            }
                        }
                        if (!found) {
                            response.println("null");
                            response.flush();
                        }
                        break;
                    }
                    case "upload": {
                        uploadHandler(data, response);
                        break;
                    }
                    case "delete": {
                        deleteHandler(data, response);
                        break;
                    }
                    case "server": {
                        List<FileInfoData> files = new ArrayList<>();
                        String result = new WebPageData(nodes, database.selectAll(FileInfoData.class)).toJSON();
                        response.println(result);
                        response.flush();
                        break;
                    }
                }
                response.flush();
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private static NodeData getIdleNode(NodeData extract) {
        if (nodes.size() == 0)
            return null;
        if (nodes.size() == 1 || !(Objects.equals(nodes.get(0), extract)))
            return nodes.get(0);
        else
            return nodes.get(1);
    }

    private static boolean addNode(NodeData node) {
        int index;
        if ((index = nodes.indexOf(node)) != -1) {
            nodes.get(index).reset(node);
            return false;
        }
        nodes.add(node);
        return true;
    }


    public static void main(String[] args) {
        Timer timer = new Timer();
        timer.schedule(new CountDown(), 5000, 5000);
        DatagramSocket UDPSocket = null;
        try {
            UDPSocket = new DatagramSocket(Integer.parseInt(Config.getProp("udpPort", "4000")));
            new UDPListener(UDPSocket).start();
        } catch (SocketException e) {
            e.printStackTrace();
        }
        try {
            ServerSocket queryListener = new ServerSocket(Integer.parseInt(Config.getProp("fileServerPort", "6500")));
            ExecutorService queryHandler = Executors.newFixedThreadPool(4);
            while (true) {
                Socket socket = queryListener.accept();
                queryHandler.submit(new DataBaseListener(socket));
            }

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
